agora inbox for [email protected]
help / color / mirror / Atom feedSome additions/#ifdefs to target new OS NetWare
1738+ messages / 3 participants
[nested] [flat]
* Some additions/#ifdefs to target new OS NetWare
@ 2002-03-04 13:02 Ulrich Neumann <[email protected]>
2002-03-04 15:27 ` Re: Some additions/#ifdefs to target new OS NetWare Tom Lane <[email protected]>
0 siblings, 1 reply; 1738+ messages in thread
From: Ulrich Neumann @ 2002-03-04 13:02 UTC (permalink / raw)
To: pgsql-hackers
Hello,
I am working on the port of PostgreSQL to NetWare.
During my work i�ve found the following that need to be added to
the sources in order to compile postgreSQL on NetWare. There
may be some more changes i�ve not seen until today, but if so
i�ll post the upcomping issues here.
1. The following additions to main.c:
#ifdef N_PLAT_NLM
/* NetWare-specific actions on startup */
NWInit();
#endif /* N_PLAT_NLM */
and
#if !defined(__BEOS__) && !defined(N_PLAT_NLM)
if (geteuid() == 0)
{
...
2. in bootstrap.c/cleanup()
static void
cleanup()
{
static int beenhere = 0;
if (!beenhere)
beenhere = 1;
else
{
elog(FATAL, "Memory manager fault: cleanup called twice.\n");
proc_exit(1);
}
if (reldesc != (Relation) NULL)
heap_close(reldesc, NoLock);
CommitTransactionCommand();
#ifdef N_PLAT_NLM
NWCleanUp();
#endif
proc_exit(Warnings);
}
3. in xlog.c
#if !defined(__BEOS__) && !defined(N_PLAT_NLM)
if (link(tmppath, path) < 0)
elog(STOP, "link from %s to %s (initialization of log file %u, segment %u) failed: %m",
tmppath, path, log, seg);
unlink(tmppath);
4. in fd.c/filepath()
static char *
filepath(const char *filename)
{
char *buf;
#ifdef N_PLAT_NLM
buf = (char *) palloc(strlen(filename) + 1);
strcpy(buf, filename);
#else
/* Not an absolute path name? Then fill in with database path... */
if (*filename != '/')
{
...
5. in dfmgr.c/load_external_function()
#ifdef N_PLAT_NLM
file_scanner->handle = dlopen(filename, RTLD_LAZY);
#else
file_scanner->handle = pg_dlopen(filename);
#endif
6. in datetime.h
#if !defined(__CYGWIN__) && !defined(N_PLAT_NLM)
#define TIMEZONE_GLOBAL timezone
#else
#define TIMEZONE_GLOBAL _timezone
#endif
7. in dynamic_loader.h
#ifndef N_PLAT_NLM
extern void *pg_dlopen(char *filename);
extern PGFunction pg_dlsym(void *handle, char *funcname);
extern void pg_dlclose(void *handle);
extern char *pg_dlerror(void);
#endif
8. a directory backend/port/netware for some OS specific files I want to maintain.
Please give me some information how I can get access to such a directory and how
I can check in files to this directory. If you have another way please let me know.
As you see these are only some simple #ifdefs that need to be added to the code.
best regards
Ulrich Neumann
Novell Worldwide Developer Support
^ permalink raw reply [nested|flat] 1738+ messages in thread
* Re: Some additions/#ifdefs to target new OS NetWare
2002-03-04 13:02 Some additions/#ifdefs to target new OS NetWare Ulrich Neumann <[email protected]>
@ 2002-03-04 15:27 ` Tom Lane <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Tom Lane @ 2002-03-04 15:27 UTC (permalink / raw)
To: Ulrich Neumann <[email protected]>; +Cc: pgsql-hackers
Ulrich Neumann <[email protected]> writes:
> During my work i�ve found the following that need to be added to
> the sources in order to compile postgreSQL on NetWare.
We'd appreciate a patch (diff -c format), not random snippets of code.
> 2. in bootstrap.c/cleanup()
> #ifdef N_PLAT_NLM
> NWCleanUp();
> #endif
Unlikely to be the right place for it, if it's needed at all which I
doubt. (Surely NetWare can manage to provide a *standard* C execution
environment, in which any platform-specific startup and cleanup stuff
is done in the C library?)
> 4. in fd.c/filepath()
> #ifdef N_PLAT_NLM
> buf = (char *) palloc(strlen(filename) + 1);
> strcpy(buf, filename);
> #else
I don't believe this either.
> 7. in dynamic_loader.h
> #ifndef N_PLAT_NLM
> extern void *pg_dlopen(char *filename);
> extern PGFunction pg_dlsym(void *handle, char *funcname);
> extern void pg_dlclose(void *handle);
> extern char *pg_dlerror(void);
> #endif
Nope. Make a platform-specific implementation of pg_dlopen and friends,
just like all the other platforms have done.
regards, tom lane
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 67424fe3b0c..3c3b35910e1 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3890,6 +3891,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4092,6 +4095,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5035,3 +5040,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 9f1dd55213d..0a8a2772b23 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -20,6 +20,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--zB/Wek73NRZcta1g--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
* [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions
@ 2026-04-27 14:23 Bertrand Drouvot <[email protected]>
0 siblings, 0 replies; 1738+ messages in thread
From: Bertrand Drouvot @ 2026-04-27 14:23 UTC (permalink / raw)
Add instrumentation under USE_ASSERT_CHECKING to detect cases where
object_aclcheck() is called on a referenced object before a lock is held on it,
which would widen the TOCTOU window between the permission check and the dependency
recording.
In recordMultipleDependencies() and changeDependencyFor(), for each referenced
object that had a permission check earlier in the same statement, assert that a
lock is already held. This catches any future code that adds a permission check
on a referenced object without first acquiring a lock.
The tracking records each (classId, objectId, mode) from object_aclcheck() and
pg_class_aclcheck(), filtering to only dependency-relevant ACL modes (ACL_CREATE,
ACL_USAGE on non namespaces, ACL_EXECUTE, ACL_TRIGGER, ACL_REFERENCES). Tracking
resets at each ProcessUtility() call. The tracking array is capped at 1024 entries
per statement, which is sufficient for any practical DDL command.
Author: Bertrand Drouvot <[email protected]>
Reviewed-by:
Discussion: https://postgr.es/m/[email protected]
---
src/backend/catalog/aclchk.c | 32 ++++++++++++
src/backend/catalog/pg_depend.c | 52 ++++++++++++++++++++
src/backend/tcop/utility.c | 3 ++
src/include/catalog/aclcheck_track.h | 73 ++++++++++++++++++++++++++++
src/tools/pgindent/typedefs.list | 1 +
5 files changed, 161 insertions(+)
51.5% src/backend/catalog/
46.7% src/include/catalog/
diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c
index 007ede997c5..d13667f38a8 100644
--- a/src/backend/catalog/aclchk.c
+++ b/src/backend/catalog/aclchk.c
@@ -76,6 +76,7 @@
#include "parser/parse_type.h"
#include "storage/lmgr.h"
#include "utils/acl.h"
+#include "catalog/aclcheck_track.h"
#include "utils/aclchk_internal.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
@@ -3891,6 +3892,8 @@ object_aclcheck_ext(Oid classid, Oid objectid,
Oid roleid, AclMode mode,
bool *is_missing)
{
+ aclcheck_track_record(classid, objectid, mode);
+
if (object_aclmask_ext(classid, objectid, roleid, mode, ACLMASK_ANY,
is_missing) != 0)
return ACLCHECK_OK;
@@ -4093,6 +4096,8 @@ AclResult
pg_class_aclcheck_ext(Oid table_oid, Oid roleid,
AclMode mode, bool *is_missing)
{
+ aclcheck_track_record(RelationRelationId, table_oid, mode);
+
if (pg_class_aclmask_ext(table_oid, roleid, mode,
ACLMASK_ANY, is_missing) != 0)
return ACLCHECK_OK;
@@ -5036,3 +5041,30 @@ RemoveRoleFromInitPriv(Oid roleid, Oid classid, Oid objid, int32 objsubid)
table_close(rel, RowExclusiveLock);
}
+
+/*
+ * Instrumentation to detect permission check before lock regressions.
+ * Only used in assert-enabled builds.
+ */
+#ifdef USE_ASSERT_CHECKING
+AclCheckEntry aclcheck_tracked[ACLCHECK_TRACK_MAX];
+int aclcheck_tracked_count = 0;
+
+void
+aclcheck_track_reset(void)
+{
+ aclcheck_tracked_count = 0;
+}
+
+bool
+aclcheck_track_was_checked(Oid classId, Oid objectId)
+{
+ for (int i = 0; i < aclcheck_tracked_count; i++)
+ {
+ if (aclcheck_tracked[i].classId == classId &&
+ aclcheck_tracked[i].objectId == objectId)
+ return true;
+ }
+ return false;
+}
+#endif /* USE_ASSERT_CHECKING */
diff --git a/src/backend/catalog/pg_depend.c b/src/backend/catalog/pg_depend.c
index 5618e3d26fa..28fa99b2d42 100644
--- a/src/backend/catalog/pg_depend.c
+++ b/src/backend/catalog/pg_depend.c
@@ -18,6 +18,7 @@
#include "access/htup_details.h"
#include "access/table.h"
#include "catalog/catalog.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_constraint.h"
@@ -27,6 +28,8 @@
#include "catalog/partition.h"
#include "commands/extension.h"
#include "miscadmin.h"
+#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/fmgroids.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
@@ -107,6 +110,31 @@ recordMultipleDependencies(const ObjectAddress *depender,
if (isObjectPinned(referenced))
continue;
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not
+ * after. The latter would widen the TOCTOU window between the
+ * permission check and the dependency recording.
+ */
+ if (aclcheck_track_was_checked(referenced->classId, referenced->objectId))
+ {
+ if (referenced->classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(referenced->objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ referenced->classId,
+ referenced->objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
+
/*
* Acquire a lock and check object still exists while recording the
* dependency.
@@ -511,6 +539,30 @@ changeDependencyFor(Oid classId, Oid objectId,
return 1;
}
+#ifdef USE_ASSERT_CHECKING
+ /*
+ * If this referenced object had a permission check earlier in this
+ * statement, assert that a lock is already held on it. This ensures
+ * callers acquired the lock before calling object_aclcheck(), not after.
+ * The latter would widen the TOCTOU window between the permission check and
+ * the dependency recording.
+ */
+ if (aclcheck_track_was_checked(objAddr.classId, objAddr.objectId))
+ {
+ if (objAddr.classId == RelationRelationId)
+ Assert(CheckRelationOidLockedByMe(objAddr.objectId,
+ AccessShareLock, true));
+ else
+ {
+ LOCKTAG tag;
+
+ SET_LOCKTAG_OBJECT(tag, MyDatabaseId,
+ objAddr.classId,
+ objAddr.objectId, 0);
+ Assert(LockHeldByMe(&tag, AccessShareLock, true));
+ }
+ }
+#endif
/*
* Acquire a lock and check object still exists while changing the
* dependency.
diff --git a/src/backend/tcop/utility.c b/src/backend/tcop/utility.c
index 73a56f1df1d..0c4d382bd37 100644
--- a/src/backend/tcop/utility.c
+++ b/src/backend/tcop/utility.c
@@ -21,6 +21,7 @@
#include "access/xact.h"
#include "access/xlog.h"
#include "catalog/namespace.h"
+#include "catalog/aclcheck_track.h"
#include "catalog/pg_authid.h"
#include "catalog/pg_inherits.h"
#include "catalog/toasting.h"
@@ -515,6 +516,8 @@ ProcessUtility(PlannedStmt *pstmt,
Assert(queryString != NULL); /* required as of 8.4 */
Assert(qc == NULL || qc->commandTag == CMDTAG_UNKNOWN);
+ aclcheck_track_reset();
+
/*
* We provide a function hook variable that lets loadable plugins get
* control when ProcessUtility is called. Such a plugin would normally
diff --git a/src/include/catalog/aclcheck_track.h b/src/include/catalog/aclcheck_track.h
new file mode 100644
index 00000000000..5825d7398d3
--- /dev/null
+++ b/src/include/catalog/aclcheck_track.h
@@ -0,0 +1,73 @@
+/*-------------------------------------------------------------------------
+ *
+ * aclcheck_track.h
+ * Instrumentation to detect permission check before lock via Assert in
+ * recordMultipleDependencies() and changeDependencyFor().
+ *
+ * Only active in USE_ASSERT_CHECKING builds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/catalog/aclcheck_track.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef ACLCHECK_TRACK_H
+#define ACLCHECK_TRACK_H
+
+#ifdef USE_ASSERT_CHECKING
+
+#include "catalog/pg_namespace.h"
+
+#define ACLCHECK_TRACK_MAX 1024
+
+typedef struct AclCheckEntry
+{
+ Oid classId;
+ Oid objectId;
+} AclCheckEntry;
+
+extern AclCheckEntry aclcheck_tracked[];
+extern int aclcheck_tracked_count;
+
+extern void aclcheck_track_reset(void);
+extern bool aclcheck_track_was_checked(Oid classId, Oid objectId);
+
+/*
+ * Only record aclchecks that are dependency-relevant:
+ * - ACL_CREATE on any object (creating something in a container)
+ * - ACL_USAGE on non-namespace objects (using a type, language, server)
+ * - ACL_EXECUTE on functions
+ * - ACL_TRIGGER, ACL_REFERENCES on relations
+ *
+ * Skip ACL_USAGE on namespaces — that's name resolution, not dependency.
+ */
+static inline void
+aclcheck_track_record(Oid classId, Oid objectId, AclMode mode)
+{
+ /* Skip ACL_USAGE on namespaces (name resolution, not dependency) */
+ if (classId == NamespaceRelationId && !(mode & ACL_CREATE))
+ return;
+
+ /* Only track dependency-relevant modes */
+ if (!(mode & (ACL_CREATE | ACL_USAGE | ACL_EXECUTE | ACL_TRIGGER | ACL_REFERENCES)))
+ return;
+
+ if (aclcheck_tracked_count < ACLCHECK_TRACK_MAX)
+ {
+ aclcheck_tracked[aclcheck_tracked_count].classId = classId;
+ aclcheck_tracked[aclcheck_tracked_count].objectId = objectId;
+ aclcheck_tracked_count++;
+ }
+}
+
+#else /* !USE_ASSERT_CHECKING */
+
+#define aclcheck_track_reset() ((void) 0)
+#define aclcheck_track_record(c, o, m) ((void) 0)
+#define aclcheck_track_was_checked(c, o) (false)
+
+#endif /* USE_ASSERT_CHECKING */
+
+#endif /* ACLCHECK_TRACK_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 8cf40c87043..cb0cc25bda8 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -19,6 +19,7 @@ AbsoluteTime
AccessMethodInfo
AccessPriv
Acl
+AclCheckEntry
AclItem
AclMaskHow
AclMode
--
2.34.1
--FjYZKkuBdH+F20sI--
^ permalink raw reply [nested|flat] 1738+ messages in thread
end of thread, other threads:[~2026-04-27 14:23 UTC | newest]
Thread overview: 1738+ messages (download: mbox mbox.gz follow: Atom feed)
-- links below jump to the message on this page --
2002-03-04 13:02 Some additions/#ifdefs to target new OS NetWare Ulrich Neumann <[email protected]>
2002-03-04 15:27 ` Tom Lane <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v20 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
2026-04-27 14:23 [PATCH v21 3/3] Add Assert guard to detect permission check before lock regressions Bertrand Drouvot <[email protected]>
This inbox is served by agora; see mirroring instructions
for how to clone and mirror all data and code used for this inbox